import 'dart:async';

import 'package:flutter/material.dart';

import 'form_input.dart';

class FormInputAutocomplete<T extends Object> extends StatefulWidget {
  const FormInputAutocomplete(
      {super.key,
      this.width,
      this.initialValue,
      required this.onSelected,
      this.hintText,
      this.validator,
      required this.onFilter,
      this.onChanged,
      this.focusNode,
      this.whenLostFocusIfUnselectResetValue = false,
      this.optionBuilder,
      this.maxLength,
      this.maxLines = 1,
      this.controller});
  final double? width;
  final String? initialValue;

  final ValueChanged<T> onSelected;
  final Future<List<T>> Function(String keyword) onFilter;
  final ValueChanged<String>? onChanged;
  final String? hintText;
  final FormFieldValidator<String>? validator;
  final FocusNode? focusNode;
  final bool whenLostFocusIfUnselectResetValue;
  final ListTile Function(T option)? optionBuilder;
  final int? maxLength;
  final int? maxLines;
  final TextEditingController? controller;
  @override
  State<StatefulWidget> createState() => _FormInputAutocompleteState<T>();
}

class _FormInputAutocompleteState<T extends Object>
    extends State<FormInputAutocomplete<T>> {
  final LayerLink _layerLink = LayerLink();
  FocusNode? _currentFocusNode;
  bool _needDisposeFocusNode = false;
  late TextEditingController _currentTextController;
  bool _selected = false;
  bool _hasOpenedOverlay = false;
  OverlayEntry? _overlayEntry;
  List<T> _options = [];
  Timer? _debounce;

  bool _controllerCreatedBySelf = false;
  @override
  void initState() {
    super.initState();

    if (widget.controller != null) {
      _controllerCreatedBySelf = false;
      _currentTextController = widget.controller!;
    } else {
      _currentTextController = TextEditingController(text: widget.initialValue);
      _controllerCreatedBySelf = true;
    }

    _attachFocusChangedEvent(widget.focusNode);
  }

  void _dispose() {
    if (_currentFocusNode != null) {
      _currentFocusNode!.removeListener(_onFocusChanged);
      if (_needDisposeFocusNode) {
        _currentFocusNode!.dispose();
      }
      _currentFocusNode = null;
    }
  }

  @override
  void dispose() {
    _dispose();
    _debounce?.cancel();
    if (_controllerCreatedBySelf) {
      _currentTextController.dispose();
    }
    super.dispose();
  }

  @override
  void didUpdateWidget(covariant FormInputAutocomplete<T> oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.controller != oldWidget.controller) {
      if (widget.controller == null) {
        _currentTextController =
            TextEditingController(text: widget.initialValue);
        _controllerCreatedBySelf = true;
      } else {
        _controllerCreatedBySelf = false;
        _currentTextController = widget.controller!;
      }
    } else {
      if (oldWidget.initialValue != widget.initialValue) {
        _currentTextController =
            TextEditingController(text: widget.initialValue);
        _controllerCreatedBySelf = true;
      }
    }

    if (oldWidget.focusNode != widget.focusNode) {
      //_dispose();
      _attachFocusChangedEvent(widget.focusNode);
    }
  }

  void rebuildOverlay() {
    if (_overlayEntry != null) {
      _overlayEntry!.markNeedsBuild();
    }
  }

  List<Widget> _buildOptionWraps(bool reverse) {
    List<Widget> widgets = [];

    for (int i = 0; i < _options.length; i++) {
      widgets.add(InkWell(
          onTap: () {
            _onItemTapped(_options[i]);
          },
          child: Text(softWrap: false, _options[i].toString())));
    }
    //}

    return widgets;
  }

  void _onItemTapped(T option) {
    _selected = true;
    widget.onSelected(option);

    closeOverlay();
    _currentTextController.text = option.toString();
    if (_currentFocusNode != null && _currentFocusNode!.hasFocus) {
      _currentFocusNode!.unfocus();
    }
  }

  void openOverlay() {
    if (_overlayEntry == null && _options.isNotEmpty) {
      RenderBox renderBox = context.findRenderObject() as RenderBox;
      Size size = renderBox.size;
      Offset offset = renderBox.localToGlobal(Offset.zero);
      Size screenSize = MediaQuery.of(context).size;
      double top = offset.dy + size.height;
      bool reverse = false;
      bool isDesktop = true;
      double optionsMaxHeight = isDesktop ? 180 : 80;

      // final viewInsets = EdgeInsets.fromViewPadding(
      //     WidgetsBinding.instance.window.viewInsets,
      //     WidgetsBinding.instance.window.devicePixelRatio);
      final viewInsets = View.of(context).viewInsets;
      final double viewBottom = viewInsets.bottom;

      if (screenSize.height - viewBottom - (offset.dy + size.height) <
          optionsMaxHeight) {
        top = offset.dy - optionsMaxHeight;
        reverse = true;
      }

      _overlayEntry ??= OverlayEntry(
          builder: (context) => Positioned(
              left: offset.dx,
              top: top,
              width: size.width,
              child: CompositedTransformFollower(
                  link: _layerLink,
                  showWhenUnlinked: false,
                  offset: reverse
                      ? Offset(0.0, -optionsMaxHeight)
                      : Offset(0.0, size.height),
                  child: Container(
                      constraints: BoxConstraints(maxHeight: optionsMaxHeight),
                      height: optionsMaxHeight,
                      child: Material(
                        elevation: 6,
                        borderOnForeground: true,
                        child: isDesktop
                            ? ListView.separated(
                                reverse: reverse,
                                padding: const EdgeInsets.all(0),
                                itemCount: _options.length,
                                itemBuilder: (BuildContext context, int index) {
                                  final T option = _options.elementAt(index);
                                  ListTile? listTileBuilded =
                                      widget.optionBuilder != null
                                          ? widget.optionBuilder!(option)
                                          : null;

                                  return ListTile(
                                    title: listTileBuilded != null
                                        ? listTileBuilded.title
                                        : Text(option.toString()),
                                    subtitle: listTileBuilded?.subtitle,
                                    onTap: () {
                                      _onItemTapped(option);
                                    },
                                  );
                                },
                                separatorBuilder:
                                    (BuildContext context, int index) {
                                  return const Divider(
                                    height: 1,
                                    thickness: 1,
                                  );
                                },
                              )
                            : Padding(
                                padding: const EdgeInsets.all(8),
                                child: SingleChildScrollView(
                                  reverse: reverse,
                                  child: Wrap(
                                    verticalDirection: reverse
                                        ? VerticalDirection.up
                                        : VerticalDirection.down,
                                    spacing: 8,
                                    children: _buildOptionWraps(reverse),
                                  ),
                                ),
                              ),
                      )))));
    }
    if (!_hasOpenedOverlay && _options.isNotEmpty) {
      Overlay.of(context).insert(_overlayEntry!);
      setState(() => _hasOpenedOverlay = true);
    }
  }

  void closeOverlay() {
    if (_hasOpenedOverlay) {
      if (mounted) {
        _overlayEntry!.remove();
        _overlayEntry = null;

        setState(() => _hasOpenedOverlay = false);
      }
    }
  }

  void _attachFocusChangedEvent(FocusNode? focusNode) {
    if (_currentFocusNode != focusNode) {
      if (_currentFocusNode != null) {
        _currentFocusNode!.removeListener(_onFocusChanged);
        _currentFocusNode = null;
      }
    }

    if (focusNode != null) {
      _currentFocusNode = focusNode;
    } else {
      _needDisposeFocusNode = true;
      _currentFocusNode = FocusNode();
    }

    _currentFocusNode!.addListener(_onFocusChanged);
  }

  void _onFocusChanged() {
    if (_currentFocusNode!.hasFocus == false) {
      Timer(const Duration(milliseconds: 200), () {
        if (widget.whenLostFocusIfUnselectResetValue && _selected == false) {
          if (widget.initialValue != null) {
            _currentTextController.text = widget.initialValue!;
          }
        }
        closeOverlay();
      });
    }
  }

  void _onInputChanged(String value) async {
    if (value.isEmpty) {
      _options.clear();
      closeOverlay();
    } else {
      _options = await widget.onFilter(value);
      if (_options.isEmpty) {
        closeOverlay();
      } else {
        if (_hasOpenedOverlay) {
          rebuildOverlay();
        } else {
          openOverlay();
        }
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return CompositedTransformTarget(
        link: _layerLink,
        child: Semantics(
          container: true,
          child: FormInput(
            hintText: widget.hintText,
            width: widget.width,
            focusNode: _currentFocusNode,
            maxLines: widget.maxLines,
            maxLength: widget.maxLength,
            validator: widget.validator,
            onFieldSubmitted: (value) {
              closeOverlay();
            },
            onChanged: (value) async {
              _selected = false;

              if (widget.onChanged != null) {
                widget.onChanged!(value);
              }

              if (_debounce?.isActive ?? false) _debounce?.cancel();

              _debounce = Timer(const Duration(milliseconds: 200), () {
                _onInputChanged(value);
              });
            },
            controller: _currentTextController,
          ),
        ));
  }
}
